fix(spec,cli): walk a slotted page's slots and a tabs panel's items[].children, and give dashboard global filters a bundle group - #16883
Conversation
…d dashboards.*.globalFilters Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…st typing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…undary pin Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…8n-unaddressable-surfaces
…aches under slots; declare the platform-objects bundle rows The platform-objects extract config explained its missing `pages` key by the shared walk rooting at `regions[].components[]` only. The walk now also roots at `slots.<slot>`, so the reason moved: it reaches every component on the three shipped record pages, and none of them carries an `id`, so the extractor still offers the page label alone. The changeset gains `@objectstack/platform-objects` — its shipped Setup bundles carry the new `dashboards.system_overview.globalFilters.created_at.label` row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 99117135566a93d57576dcce6e5f00361fa1a81f && git checkout 99117135566a93d57576dcce6e5f00361fa1a81f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa5152d1ad4194293c4550a844da7a83475697d6 205fff6c74414e5268e96932d178295ab68b1224 && git checkout -B drift-repro aa5152d1ad4194293c4550a844da7a83475697d6 && git merge --no-ff 205fff6c74414e5268e96932d178295ab68b1224
node scripts/docs-audit/affected-docs.mjs --json aa5152d1ad4194293c4550a844da7a83475697d6
|
⛔ Blocked, and this is the seat saying so once rather than leaving a red unattended — 2026-09-08T13:51Z
What is red, and why it is a fair red
⇒ The changeset declares ⭐ The gate is right and the declaration is the part that is correct. Why it is not fixed in this commentThe That leaves no route that is both available and legal:
⇒ This PR parks on tier capacity. ⛔ It is not abandoned and it is not waiting on review — it is waiting on one thing a person controls, and it resumes the moment fable capacity returns: raise the two levels, push, then the at-tier contract review this diff owes for widening What is already established, so the next dev re-derives none of itThe build itself was at tier — the dev's transcript carries 165 harness-stamped Docs drift is clean, measured by this seat (⛔ do not re-derive): Everything else on this head is green: 35 checks, The per-package readings the next push still owes
If a measurement shows one of them genuinely publishes nothing new, that is route 2 — a producer-side correction stated with its reading, ⛔ never a level bump used to silence the gate. ⛔ Still draft, ⛔ not enqueued, ⛔ auto-merge unarmed, Generated by Claude Code |
Contract review (
|
Seat concurs with the director seat's review, and corrects its own blocker comment — 2026-09-08T14:11Z
⭐ Finding 2 is real, and I verified it myself rather than concurring on the report
⇒ A breaking change to a published export, graded
|
…8n-unaddressable-surfaces
The os-regen driver merged `content/docs/references/api/protocol.mdx` with exit 0 while keeping one side; regenerating from the merged sources restores both — main's `droppedFields` prose (#16930) and this branch's `dashboards` row gaining its `globalFilters` continuation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5
…rd filter emitter The changeset graded `@objectstack/cli` and `@objectstack/platform-objects` `patch` while the diff moves each package's `src/**` and the PR declares clause ②; the maintainer ruling of 2026-09-04 (decision batch #35, on #15294) puts a purely additive widening of a published surface at `minor` or above, so both are raised. `walkAddressedPageComponents` is published and its return value changed shape, so the changeset now carries the `**BREAKING**` banner and exactly one ADR-0087 disposition, which are the only breaking-ness carriers during the launch window the level number cannot express. `collectExpectedEntries` gained a `globalFilters` emitter with no test: `check:i18n-walk-parity` measures at top-level group granularity and `dashboards` was already walked, so the sub-group could drift green. The new pin holds the `name`-keyed and `field`-keyed spellings, the option keys, the unkeyed filter that is passed over, and the `optionsFrom` filter whose label stays addressable while its fetched rows do not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5
|
Director seat adoption record — summon #20, Contract review (
|
| # | Owed | Status on 7b942e760 |
|---|---|---|
| 1 | blocking — LEVEL axis: raise @objectstack/cli and @objectstack/platform-objects from patch to minor under Clause-②: yes |
Done. .changeset/i18n-slotted-pages-and-global-filters.md:2-4 — spec/cli/platform-objects all minor. Re-run offline with origin/main's script and the live PR payload (--base ee2cb6b40 --head refs/pr-review/16883 --event …): ✓ LEVEL AXIS: declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch, exit 0. CI Check Changeset green ×2 on the head. |
| 2 | blocking — the second rule: a breaking change on a published export must carry the **BREAKING** banner and one ADR-0087 disposition marker (check-changeset-no-major.mjs:61-73) |
Done. :9 **BREAKING** (return shape) — walkAddressedPageComponents … returns { regions?, slots? } with the one-line migration; :22 the argument; :24 exactly one marker <!-- adr-0087: not-required (no-migration-prescription) … -->, category ∈ CATEGORIES (check-adr-0087-registration.mjs:467-473). Offline re-run of that gate on the head: ✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING] not-required (no-migration-prescription), exit 0. |
| 3 | record — pin the CLI globalFilters emitter |
Done. packages/cli/test/i18n-extract.test.ts:278-366, four its against collectExpectedEntries: name-else-field keys + option keys; source values; no key under an overridden field and nothing for an unkeyed filter; optionsFrom label-only. Matched by line to the emitter packages/cli/src/utils/i18n-extract.ts:1325-1344 (globalFilterKey import :130; continue on undefined key :1334-1335; options keyed String(value) with the same `string |
| 4 | record — fix the PR body's alias overstatement | Not done. Body still reads "title / name / text for label, choices / values / items for options" (Finding B paragraph). See F1. |
| 5 | record — carry the nine outside-Zone-1 files into the PR body | Not done. No such list in the body. See F1. |
| — | Concurrence 5586497592 | Its three verified claims hold on the head: the export is published and its return shape changed (i18n-resolver.ts:1735-1738); the carriers were absent at af0593780 and are present now; api-surface/system.json is +3/−0 and structurally blind to the in-place signature change (the walkAddressedPageComponents (function) entry does not move). |
Derived judgments
- Public surface moves in three independent ways —
Clause-②: yesis right. (a) A new authorable strict group on the published translation payload:translation.zod.ts:778-790dashboards.<name>.globalFilters.<key>{ label?, options?: Record<string,string> }— a bundle carrying it was refused by the strict object before (pinned by theCONTROL: … 17.3-shaped bundletest). Aliases at:703(filters/globalFilter) and:785are hint-only on a strictObject; the accept set widens by the key alone. (b) Index +3 inpackages/spec/api-surface/system.json/export-origins/system.json:AddressedPageRoots (type)i18n-resolver.ts:1682,GlobalFilterLike (interface):1152,globalFilterKey (function):1187. (c)walkAddressedPageComponents:1735-1738: parameter widenedPick<PageLike,'regions'>→AddressedPageRoots(accept set widens), return changedPageLike['regions']→AddressedPageRoots(breaking for any return-reader). Additive optional members:PageLike.slots?:1530,DashboardLike.globalFilters?:1173. - Walker coverage, re-read on the head. Roots: regions then slots, each slot one component or an array (
:1750-1766root-id claim;:1858-1876rebuild), matchingPageSchema.slotsper-slot unionpage.zod.ts:739-753. Descent:properties.childrenand anyitems[]entry carrying achildrenarray (:1791-1822), matchingPageTabsProps.items[].childrencomponent.zod.ts:758andPageAccordionProps:1762;body/footerstay undescended. Depth cap is the first line ofwalkComposition(:1791), cycle guard unchanged (:1830-1836),translatePagestops at region-level components, so copy on components nested in another component'sproperties.childrenis authorable but never resolved — 4 KPI labels stay English on hotCRM's zh-CN landing page #12961 arbitration now root-wins-outright (:1840-1842).translatePagedestructures:1987and carriesslotsback:2063.translateDashboardoverlays filters identity-preservingly and leavesglobalFiltersoff the copy when nothing resolved (:1231-1270,:1329-1340). Extractor root header route now asks the shared walk (:1366). - Key derivation premise holds at head:
GlobalFilterSchema.name"defaults to field"dashboard.zod.ts:801,fieldrequired:825→globalFilterKey(name, elsefield, elseundefined) is the schema's own declaration, not a lenient fallback. - Bundles match authored filters: platform-objects
system_overviewauthorsfield: 'created_at', noname(system_overview.dashboard.ts:236-238) →created_atkey in en/es/ja/zh; showcaseregion/task_statusmatchrevenue-pulse.dashboard.ts:40/ops-dashboard.dashboard.ts:67; zh-CN rows are translations. - Consumers: in-repo
translatePage+ CLI extractor, both updated. objectui at the pinned.objectui-sha53ded82b(unchanged by the merge; same onorigin/main):git grepfor the four symbols → 0 files; positive controlpickLocalized→ 73 files. The breaking half is a third-party concern only. - Card delivery vs acceptance: A — slotted-page walk count non-zero and tab-panel contents addressable (tests
walks regions[].components[] AND slots.<slot>…,descends into items[].children…,resolves each authored key — slots roots, tab panels…). The tab strip's ownitems[].labelremains unaddressable, pinned as BOUNDARY — the card's "each authored key resolves" is delivered for component keys, not strip labels. B — filter label + static option labels delivered;optionsFromexcluded by design. Docs:content/docs/ui/translations.mdx:80-81rows accurate againstPAGE_COMPONENT_COPY_KEYS:1576-1578; generated references regenerated; nocontent/docs/releases/**edit. - Dev report 5586199586 open questions (no patch-round report exists — that dev died at ~03:29Z per 5597046128): OQ1 (tab strip label key) → A, leave as-is: the card's suggested shape does not include it, both keyings are unstable while
valueis optional, the BOUNDARY pin records it — and F3 below must be fixed before any option-B visitor is attempted. OQ2 (minor+ pair return) → A, and now correctly carried:minoris right under the launch-window rule only with the banner + disposition, which the head has (:9,:24); the{ regions, slots }pair is the right shape (a sibling array-returning export would rebuild regions and leave translatedslotsbehind — the partial-rebuild trap the report names); the migration line at:9/:13is adequate. Nodeviationsdeclared; theout_of_scope_findingsare noted-not-filed and stay that way.
Semver / changeset
- One changeset:
.changeset/i18n-slotted-pages-and-global-filters.md—@objectstack/spec: minor,@objectstack/cli: minor,@objectstack/platform-objects: minor. - Rule applied: LEVEL axis (
check-changeset-no-major.mjs:5-16, [finding] No gate answers whether a changeset's LEVEL fits the surface — Check Changeset is green on patch and on minor for the same diff #16055/[finding]Check Changeset's clause-② level rule is PR-scoped but the fact it judges is package-scoped — a PR that widens package A is refused for grading package Bpatch, where B only received a comment #16361) needs ≥1 moved package atminor+ under ayes; the WHICH LEVEL rule (pr-automation.yml:712-719) makes a purely additive widening of a published surface at leastminorper package. All three packages widen: spec (group + 3 exports + signature), cli (new emitted key family:1325-1344, root-header route:1366), platform-objects (new bundle rows in four shipped Setup bundles).majoris refused during the launch window (:47-50);minoris the correct level. - Breaking carriers:
**BREAKING**banner:9; ADR-0087 marker:24(exactly one;readDispositionrefuses >1).type-surface-only— the only category exempt from the prescription refusal (ADR-0087 D8:772) — is closed to this PR by predicate 2 (diff touchespackages/spec/**, gate:3021-3031), sono-migration-prescriptionwith the:22argument is the honest available disposition, as the prior review prescribed. See F5 for the residual. - Both gates green in CI on the head and re-derived offline on
origin/main's scripts (which post-date CI's base92949593d:check-adr-0087-registration.mjsmoved in fix(scripts): a dotted member path's last segment resolves at the region's top depth #17001/Generate the lockstep package count instead of hand-typing it #17055 — 0 changed lines touch the prescription arm;check-changeset-no-major.mjsunchanged).
Boundary flags
- Governed paths: none of the 21 files is under
.claude/**,skills/**,docs/adr/**,AGENTS.md,CLAUDE.md, orcontent/docs/releases/**(grep over the diff file list: no match).docs/audits/2026-07-unknown-key-strictness-ledger.counts.mdis a regenerated ledger (350→351, one new strictObject).Governed Surface Queue Guardgreen. - Tier of the corrective push:
2a347ac7and7b942e760carryCo-Authored-By: Claude Opus 5— belowCONTRACT_REVIEW_TIER, as the claim 5594626301 declared. Their scope is confined to the changeset, one new test file and one regenerated doc line (interdiff-proven); no contract code moved at that tier. This at-tier review is the compensating control. - Record integrity: recovery comment 5597046128 states "the PR body was updated at 03:19Z". Measured false: the 03:19:19Z timeline event is
labeled needs:contract-review(yinlianghui); the body is 12508 bytes, the exact size the dev report recorded at creation, and still carries the pre-fix changeset paragraph. GraphQL edit history is not served to this seat; the REST timeline and byte count are the evidence. - Card state:
pm:queue, assignee cleared (dead-claim recovery); PR draft;needs:contract-reviewon both carriers (check-clause2-carrierspair agrees: label present, body lineClause-②: yes). No patch-roundos-dev-report; CI is the only measurement of7b942e760— acceptable given the diff sinceaf0593780is test + changeset only. - Merge path: maintainer-only stands (published contract widened three ways plus a breaking return shape on a published function). Main has not touched any of the PR's 21 files since the merge-base;
mergeable_statewasunknownat read time (not yet computed by GitHub). - Docs-drift bot's two flagged pages (
validating-metadata.mdx,dashboards.mdx) were not re-derived here; neither is in the diff.
Findings
- F1 — non-blocking (must be edited before the maintainer reads it; no push needed): the PR body is stale on three counts — its "Changeset:" paragraph still says
@objectstack/clipatch,@objectstack/platform-objectspatch (contradicting.changeset/…:3-4), it never mentions the**BREAKING**banner / ADR-0087 disposition or the new pin, and the Finding B paragraph still overstates the alias mirror (text,itemsare notGlobalFilterSchemaaliases —dashboard.zod.ts:787;text→labelis the option-row alias:855). Owed record items 4 and 5 are therefore undelivered; the nine outside-Zone-1 files (CLI extractor + two tests, platform-objects extract config + 4 bundles, showcase bundle,translations.mdx, changeset) still belong in the body. - F2 — non-blocking: stale docblock in a touched file.
packages/cli/src/utils/i18n-extract.ts:1072-1075says lint'swalkPageComponentsis "WIDER than the resolver in four ways (slots.<slot>roots,properties.items[].children,properties.body,properties.footer)" — after this PR it is wider in two;:1067"counts its id as region-level" is now root-level. - F3 — non-blocking (contract docblock accuracy):
i18n-resolver.ts:1846-1848sayschildrenanditems[].childrenare "the slots the walk owns; everything else on the node is the visitor's". In fact:1814-1822rebuilds the wholeitemsarray from the original component's panels and:1853spreads it overnext.properties, so any visitor edit to anotheritems[*]key (e.g. a panellabel) is silently discarded. No current visitor writesitems(translatePageoverlay:2012-2034writes copy keys only; the extractor returns the node), so no behaviour defect today — but it is exactly what OQ1's option B would trip on. Fix the sentence, or rebuild panels fromnext. - F4 — non-blocking (cosmetic):
translation.zod.ts:785alias table carriestext: 'label'anditems: 'options', which the dashboard schema does not alias; the code comment:781-784names onlytitle/name/choices/values. Hint-only on a strictObject; accept set unaffected. - F5 — observation: the
no-migration-prescriptiondisposition passes because the changeset's one-line consumer rewrite ("bindsconst { regions } = …") is the imperative-sentence shape the detector deliberately does not read (gate header:830-839). The vocabulary has no category for a spec-internal TS return-shape change (predicate 2 closestype-surface-only), so this is the honest available answer and matches the prior review's instruction — recorded for the maintainer, not a change request on this PR.
CI at read time
38 check runs on 7b942e760 (all head_sha validated — a sibling seat's files shared this seat's scratchpad, so every artifact was re-fetched and checked by sha); 34 latest-per-name: 30 success, 4 skipped (Auto Label, Check PR Size, Packed-tarball smoke (opt-in), Console Pin Gate), 0 failure / 0 in_progress. Check Changeset success at 02:22:03Z (push) and 03:19:24Z (labeled); Lint & Repo Gates success (02:22–02:47Z); Test Core 1–6, Type Check ×5, Dogfood ×5, Temporal Conformance, Build Core/Docs, Spec property liveness, Part-of guard, both single-claim guards all success. Job logs are not retrievable from this seat; both changeset gates were re-run offline against the head (see above), exit 0. Not waited for; nothing was pending.
Implemented-by: branch claude/issue-16772-i18n-unaddressable-surfaces
Reviewed-by: director seat summon #20 (isolated fable subagent, transcript-verified before adoption)
{"pr":16883,"head":"7b942e760f318cfeb093bd0b5d9b63b7a609e734","verdict":"PASS WITH FINDINGS","blocking":[],"clause2":"yes","semver_ok":true,"governed":false,"ci":"34 latest-per-name on head: 30 success, 4 skipped, 0 red/in_progress; Check Changeset green x2; both changeset gates re-run offline exit 0"}
Generated by Claude Code
… stale Docblocks only — no behaviour change, no test change. F2 (`packages/cli/src/utils/i18n-extract.ts`, `emitPageComponentCopy`): the shared walk this function delegates to now roots at `regions[].components[]` AND `slots.<slot>` and descends `properties.children` AND a panel's `properties.items[].children`, so the three claims written against the old, narrower walk were false on this branch: the roots/descent "only" pair, the emission exception's REGION-LEVEL wording (it is any ROOT-LEVEL `page:header`, a region's entry or a slot's — the code already reads `!nested`), and the count of ways `@objectstack/lint`'s `walkPageComponents` is wider. Re-derived against `packages/lint/src/page-walk.ts`: lint roots at slots (`:166-173`), descends `items[].children` (`:126-133`), `children` (`:139-142`) and `body` / `footer` (`:144-145`); the resolver now shares the first three, so lint is wider in exactly two ways — `properties.body` and `properties.footer`. F3 (`packages/spec/src/system/i18n-resolver.ts`, `walkAddressedPageComponents`): the docblock said `children` and `items[].children` are "the slots the walk owns; everything else on the node is the visitor's". `walkComposition` is called with the ORIGINAL `component` (`:1851`) and rebuilds the WHOLE `items` array from that original's entries (`:1814-1822` — non-panel entries copied across verbatim), then `:1853` spreads it over `next.properties`, so a visitor's edit to any other `items[*]` key is overwritten. The behaviour is deliberate and no current visitor writes `items`; only the sentence was wrong, so only the sentence changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Js5kTpTtxieBjPyScgxJ3
Landing note — the standing PASS WITH FINDINGS (5597570132, head
|
Fixes #16772
Clause-②: yes
Container & model:
M,mode:cloud,model: claude-fable-5-1(CONTRACT_REVIEW_TIER, passed explicitly on the resumption dispatch).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/system/i18n-resolver.ts packages/spec/src/system/translation.zod.ts packages/spec/api-surface/system.json packages/cli/src/utils/i18n-extract.ts, run by the dispatching seat for THIS dispatch, printed a "Clause ② SUSPECT surface" block namingpackages/spec/src/system/i18n-resolver.tsand.../translation.zod.tsunderpackages/spec/src/**— "the contract surface … the normal landing zone of a clause-② card" — plus "whichever tier is dispatched, the PR's actual diff passes the clause-② enqueue gate before the card may enqueue." This diff movespackages/spec/api-surface/system.json(+3) ⇒ it widens the published surface ⇒ 强制条款② ⇒ fable.What changed
Two surfaces the console renders that no translation bundle could address, both in
packages/spec, both the benign direction of the seam #16735 describes (nothing to address, so nothing claimed coverage).Finding A — the shared page walk now reaches a slotted page and a tabs panel.
walkAddressedPageComponents(the ONE traversal behindtranslatePageand the CLI extractor) rooted atregions[].components[]only and descendedproperties.childrenonly. Now:regions[].components[]ANDslots.SLOT(PageSchema.slotsdeclares each slot as onePageComponentSchemaor an array of them — the walk accepts both and passes any other value through). Regions first, then slots in authored key order; both are root level (depth 0,nested: false) for the ruled id arbitration and for the page-namepage:headerroute, so a slotted page'sslots.headeris translated as THE page's header by page name, and the id route is not read for it (the same exception a region-level header already had).properties.childrenAND apage:tabs/page:accordionpanel'sproperties.items[].children— matched by SHAPE (an object entry ofitemscarrying achildrenarray), becausepropertiesis an open bag. The panel object itself is not a component and is not visited; its children sit one level below the tabs node.body/footerstay undescended (renderer back-compat fallback, not an authorable spelling). Depth cap, cycle guard and thetranslatePagestops at region-level components, so copy on components nested in another component'sproperties.childrenis authorable but never resolved — 4 KPI labels stay English on hotCRM's zh-CN landing page #12961 collision arbitration are unchanged.AddressedPageRoots(a Pick ofPageLikeonregionsandslots) instead of a Pick onregionsalone, and the walk returns the rebuilt roots pair{ regions, slots }(each key present exactly when present on the input) instead of the regions array.PageLikegainsslots;translatePagecarries the rebuiltslotsback onto the document. In-repo consumers:translatePage(updated) and the CLI extractor (enumeration-only, ignores the return). objectui at the pinned.objectui-sha(53ded82b) imports none ofwalkAddressedPageComponents/AddressedPageRoots/GlobalFilterLike/globalFilterKey— measured withgit grepat that commit, positive controlpickLocalizedhit.Finding B —
dashboards.NAME.globalFilters.KEYis a new bundle group (translation.zod.ts):labelplus anoptionsmap from the optionvaluespelled as a string to its translated label. The KEY is the filter'sname, else itsfield— not a lenient fallback:GlobalFilterSchema.nameis declared as "Stable filter name (variable key); defaults to field", so a filter that authors nonameIS keyed by itsfieldeverywhere the platform reads it. The exportedglobalFilterKey()is the one derivation both the resolver and the extractor use. Alias tables, re-measured at the head: at the DASHBOARD levelfilters/globalFilter→globalFilters(translation.zod.ts:703), which do mirrorDashboardSchema's own. INSIDE the group (:785) the table isname/title/text→labelandchoices/values/items→options, of whichtitle→labelandchoices/values→optionsare theGlobalFilterSchemaaliases (dashboard.zod.ts:787);text→labelis the OPTION ROW's alias (:855), andname/itemsare not dashboard aliases at all. Every one of them is hint-only on astrictObject— an alias widens no accept set, it only names the one spelling the bundle takes — so the extra two are a cosmetic overreach in the hint table, recorded and not fixed here.optionsFromoptions are fetched rows and deliberately have no key.translateDashboardoverlays the group on the served document; only filters the bundle addresses are rebuilt, andglobalFiltersis left off the copy when none moved.@objectstack/cli(i18n-extract.ts):collectExpectedEntriesoffersdashboards.NAME.globalFilters.KEY.label/.options.VALUEfor every static filter, and the page-name header route (pages.NAME.title/.subtitle) for apage:headerat ANY root by asking the shared walk (nested: false) instead of a hand loop overpage.regionsthat would have offered nothing forslots.header. Component keys underslotsand tab panels follow from the shared walk with no extractor change.Bundles born under the ratchet:
@objectstack/platform-objectsSetup bundles (en,zh-CN,ja-JP,es-ES) gaindashboards.system_overview.globalFilters.created_at.label(the date-range bar; the filter authors noname, so it is keyed byfield); the showcase example bundle gains its two dashboards' filter rows inenandzh-CN.Docs / generated:
content/docs/ui/translations.mdx(hand-written tree) gains the two key rows;content/docs/references/**,api-surface/system.json(+3:AddressedPageRoots,GlobalFilterLike,globalFilterKey),export-origins/system.json,liveness/translation.jsonand the strictness-ledger counts file were regenerated by the repo's own tooling (check:generated: all 15 artifacts up to date after theorigin/mainmerge). The platform-objects extract-config note that explained its missingpageskey by the old root set was re-measured: the walk now reaches every component on the three shipped record pages, none of them carries anid, so the extractor still offers the page label alone (the CLI boundary pin holds both halves:reached: true,addressed: 0,offered: ['label']).Files outside
packages/spec— nine hand-authored, plus the changeset, one hand-written doc page and three regenerated artifacts:packages/cli/src/utils/i18n-extract.tsglobalFiltersemitter + the root-header route through the shared walkpackages/cli/test/i18n-extract.test.tspackages/cli/test/platform-page-i18n-parity.test.tspackages/platform-objects/scripts/i18n-extract.config.tspageskey, re-measuredpackages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.tssystem_overviewfilter rowexamples/app-showcase/src/system/translations/index.tscontent/docs/ui/translations.mdx.changeset/i18n-slotted-pages-and-global-filters.mdcontent/docs/references/api/protocol.mdx,content/docs/references/system/translation.mdx,docs/audits/2026-07-unknown-key-strictness-ledger.counts.mdThe other seven files are
packages/spec:src/system/{i18n-resolver.ts, translation.zod.ts}and their two test files, plus the regeneratedapi-surface/system.json,export-origins/system.jsonandliveness/translation.json.Changeset:
.changeset/i18n-slotted-pages-and-global-filters.md—@objectstack/specminor,@objectstack/climinor,@objectstack/platform-objectsminor (:2-4). All three move published source and all three widen it, so all three are graded the same: spec (the new authorable group, +3 exports, the signature), cli (the new emittedglobalFilterskey family and the root-header route), platform-objects (new rows in four shipped Setup bundles). The LEVEL axis ofscripts/check-changeset-no-major.mjsrequires at least one moved package atminor+ underClause-②: yes, and the WHICH-LEVEL rule grades a purely additive widening of a published surface at leastminorper package.majoris refused during the launch window ("we ship breaking changes asminor"), so the version number carries nothing about breaking-ness and the breaking half is carried in writing instead — three carriers, all in that file:**BREAKING** (return shape)banner at:9:walkAddressedPageComponentsis a published export of@objectstack/specand now returns the rebuilt roots pair{ regions?, slots? }where it returned the regions array alone. The migration is on the same line — a caller that only enumerates through the visitor is unaffected; a caller that reads the return value bindsconst { regions } = walkAddressedPageComponents(doc, visit).:24:not-required (no-migration-prescription), argued at:22. Nothing an author writes moves — the group is additive, no spec key is retired, no storedsys_metadatashape changes — soobjectstack migrate metahas nothing to act on; the one incompatible surface is a published function's TypeScript return type, which reaches every consumer through the compiler. (type-surface-only, the one category exempt from the prescription refusal, is closed to this PR: its predicate excludes a diff touchingpackages/spec/**.)packages/cli/test/i18n-extract.test.ts:278-366, four cases overcollectExpectedEntries: the key isname-else-fieldand every static option is offered; the source values carry through and an option is keyed by itsvalue; nothing is offered under afielda named filter overrode, and nothing at all for an unkeyed filter; anoptionsFromfilter gets its label and no option key.check:i18n-walk-paritymeasures at top-level group granularity anddashboardswas already walked before this group existed, so that gate cannot see a sub-group emitter drift — this pin is what holds it.check:api-surfacereports 3 added / 0 breaking because its signatures snapshot is scoped to thedefineXfactories.Resumption note
The predecessor session died on an account usage limit holding "two spec assertions failed and the CLI file failed at load". Re-run on the inherited HEAD
f3e92ee1c(dist built 11:28Z, after the last wip commit at 11:25Z) BEFORE any change:i18n-resolver.test.ts+translation.test.ts391/391 pass; the whole spec suite 465 files / 12978 tests pass;packages/cli/test/platform-page-i18n-parity.test.ts27/27 pass. The failure bodies could not be reproduced on the committed tree — they belonged to a transient state (uncommitted edits or a pre-build dist) that no longer exists. The three wip commits were re-read adversarially against the schema premises (PageSchema.slotsunion per slot,GlobalFilterSchema.namedefault,PageTabsProps.items[]) and stand; the one thing they missed was the platform-objects changeset entry, added here.Verification
Tree:
origin/mainc930f8597merged viabash scripts/pm/os-regen-merge.sh(merge commit718bff099; step 2 had no both-sides regen path, step 3 nothing extra to commit);pnpm install --frozen-lockfile; spec rebuilt; CLI dependency closure rebuilt (turbo, 56 tasks). Final commitaf0593780(comment + changeset only; no test input).origin/mainhas since moved 8 more commits toa5d4e286b(scripts/check-cli-test-child-env.mjsamong them) — not re-merged here; the queue rebuild covers it.Post-merge, all through
scripts/pm/os-verify-lock.sh, verdicts quoted from itsVERDICT command-exitline:pnpm --filter @objectstack/spec test—VERDICT command-exit 0· 465 files / 12989 tests passed (at718bff099)pnpm --filter @objectstack/spec typecheck— exit 0 (tsc + scripts +check:test-typecheck: OK)pnpm --filter @objectstack/cli exec vitest run --project unit—VERDICT command-exit 0· 186 files, 2556 passed | 6 expected fail;pnpm --filter @objectstack/cli typecheckOK. Theintegrationtier is declared to CI (the diff touches no integration file or spawn entry).pnpm --filter @objectstack/platform-objects test— 37 files / 545 passed;typecheckOK.pnpm --filter @objectstack/spec check:generated— exit 0, "All 15 generated artifacts are up to date".node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ranreconciles 107 derived / 107 run / 0 NOT-MEASURED / 0 UNRUN. Every family exited 0 on its own verdict line, includingcheck:i18n,check:i18n-coverage(13 configs, 621 baselined, none new),check:i18n-walk-parity(11 declared groups, 8 walked, 3 exempted),check:api-surface,check:authorable-surface,check:strictness-ledger,check:liveness,check:pm-widening-tells,check:changeset-no-major("introduces nomajorbump"; LEVEL AXIS not measured locally — no PR payload),check:nul-bytes,check:partof-closing-keywordself-test. One family needed the CI-shaped heap:check:type-check-debtOOMed under--max-old-space-size=4096(exit 3, PREREQUISITE NOT MET) and passed under 6144, the ceiling its own script pins ("5 ledger entries re-measured, none above its recorded number").eslint --no-inline-config --format jsonover the 12 changed.tsfiles ataf0593780— 12 files linted, 0 errors, 0 warnings. Population read from eslint's own config (--print-configreturns a config for the changed files, i.e. none is ignored); invariance:eslint.config.mjsstates it never enables type-aware linting for any file (noparserOptions.project, no typed rules — quoted at its line 326-332), so this diff cannot move any untouched file's verdict. The repo-widepnpm lintis CI's run.Patch rounds after
af0593780.ec85cc77mergedorigin/main(that file's only source-side change was main's own 46-line hunk ini18n-extract.ts, no conflict residue) and2a347ac7regenerated one line ofcontent/docs/references/api/protocol.mdx;7b942e760added the CLI emitter pin and raised cli / platform-objects tominor.205fff6c(this round —https://claude.ai/code/session_017Js5kTpTtxieBjPyScgxJ3) is DOCBLOCKS ONLY — no behaviour, no test, no changeset change — correcting two contracts the widened walk left stale:i18n-extract.ts'semitPageComponentCopyheader (its roots/descent "only" pair, the emission exception's REGION-LEVEL wording, and the count of ways@objectstack/lint'swalkPageComponentsis wider — two, not four:properties.bodyandproperties.footer, re-derived againstpackages/lint/src/page-walk.ts:126-145,166-173), andwalkAddressedPageComponents' pre-order comment, which saidchildren/items[].childrenare the only slots the walk owns while:1851rebuilds from the ORIGINAL component and:1853spreads the WHOLE rebuiltitemsarray overnext.properties— so a visitor's edit to any otheritems[*]key is overwritten. Behaviour deliberately unchanged (no visitor writesitems); only the sentence was wrong. Measured on205fff6c, each exit code captured before any pipe:pnpm --filter @objectstack/cli typecheck0 (check:test-typecheck: OK) ·pnpm --filter @objectstack/spec typecheck0 ·vitest run test/i18n-extract.test.ts test/platform-page-i18n-parity.test.ts2 files / 51 tests passed ·vitest run src/system/{i18n-resolver,translation}.test.ts2 files / 391 tests passed ·check:i18n-walk-parity0 (11 declared groups, 8 walked, 3 exempted) ·check:nul-bytes,check:corpus-claim-drift,check:keyed-text-bounds,check-comment-mask-adoption,check-comment-mask-corpus(6394 files, 0 disagree) all 0 ·check-changeset-no-major --base origin/main --head HEAD0 andcheck-adr-0087-registration0 (1 declared-breaking changeset, dispositionnot-required (no-migration-prescription)). Heavy runs throughscripts/pm/os-verify-lock.sh(OS_VERIFY_LOCK_SLOT=issue-16772): the CLI dependency-closure build —VERDICT command-exit 0 · held the lock 540s; the typecheck/test batch —VERDICT command-exit 0 · held the lock 148s · waited 11s; the CLI build + walk parity —VERDICT command-exit 0 · held the lock 18s · waited 280s. Lint narrowed and re-proven on this round's two files:eslint --no-inline-config --format json— 2 files linted, 0 errors, 0 warnings; the invariance argument in the bullet above is unchanged (no type-aware linting, so this diff cannot move an untouched file's verdict). The remaining derived families are unchanged fromaf0593780's 107/107 reconciliation — this round adds no path to the change set — and CI is the run of record for them.Ablation (from the committed state, each leg restored under
trap … EXIT INT TERM, landing proved by anchor counts and blob hashes, tree empty ongit diff HEADandgit status --porcelainafterwards):slotsnever a root (i18n-resolver.ts): speci18n-resolver.test.ts7 failed / 257 passed (the slots-root trace, the measured-zero fixture, cross-root arbitration, rebuild/no-mutation, and the three slotted-pagetranslatePagepins). Dist-mediated leg for the CLI pin: markerABLATION-M1-SLOTS-NEVER-A-ROOTproved INpackages/spec/distbyscripts/ablation-dist-preflight.mjsafter a rebuild, thenplatform-page-i18n-parity.test.ts3 failed / 24 passed (walk parity,slots.headerroute, the boundary pin); restore leg rebuilt,--absentpreflight "marker absent from all 218 built files", green control 27/27.items[].childrennever descended: 6 failed / 258 passed.globalFiltersgroup and its dashboard-level aliases removed fromtranslation.zod.ts:translation.test.ts8 failed / 119 passed.Source blobs after every leg equal
HEAD:(a94f3ce15…for the resolver,b6663c982…for the schema).验收备注
PageTabsProps.items[].label) still has no bundle key. It is anI18nLabelSchema, so its route is the inline locale map (the ruled authoring route, 2026-08-06); a bundle key for a panel that carries no id and whosevalueis optional is a naming decision this PR does not make. Pinned as a BOUNDARY test (translatePage — slotted page roots and tab panels, last case) so the day a key is added the pin is the one to flip. Raised as an open question in the report for the contract reviewer; 承接者:无 unless the reviewer wants it.LEVEL AXIShalf ofcheck:changeset-no-majorreads the PR payload and is measured only in CI._views.VIEW.descriptionis declared, walked and counted as covered but never rendered — plus three surfaces a bundle cannot address at all #16735 and analytics: chart category labels drawn from aselectfield's options render untranslated on a SAME-object dataset dimension, while the identical values translate on a dotted cross-object one #16773 remain open (split siblings, other lanes).🤖 Generated with Claude Code
https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Generated by Claude Code
Generated by Claude Code